import threading

class WorkerThread(threading.Thread):
    '''
    Polls an AsyncRunner using AsyncRunner._next_call() and calls the returned
    methods. Terminates when _next_call() returns None.
    '''
    def __init__(self,
                 async_runner,
                 callback_start = None,
                 callback_done = None):
        threading.Thread.__init__(self)
        self._lock = threading.Lock()
        self._name = None
        self._async_runner = async_runner
        self._callback_start = callback_start
        self._callback_done = callback_done

    def run(self):
        while True:
            call = self._async_runner._next_call()
            if not call:
                break
            with self._lock:
                self._name = call[0]
            if self._callback_start:
                self._callback_start(call)
            call[1](*call[2], **call[3])
            if self._callback_done:
                self._callback_done(call)
            with self._lock:
                self._name = None

    # Returns the name of the currently active call, or None if none is active.
    def get_active_call(self):
        with self._lock:
            return self._name

class AsyncRunner:
    ''' Runs a number of method calls asynchronously. '''
    def __init__(self,
                 num_threads,
                 callback_start = None,
                 callback_done = None):
        assert num_threads > 0
        self._num_threads = num_threads
        self._callback_start = callback_start
        self._callback_done = callback_done
        self._workers = []
        self._calls = []
        self._started = False
        self._lock = threading.Lock()

    '''
    Adds a call to "target" with the specified arguments. "call_name" is used by
    get_active_calls().
    '''
    def add_call(self, call_name, target, *args, **kwargs):
        assert callable(target), '%s is not callable' % target
        with self._lock:
            assert not self._started
        # TODO use dedicated class type instead of 4-tuple.
        self._calls.append((call_name, target, args, kwargs))

    '''
    Asynchronously performs all calls added by add_call(). The calls are
    started in the order they were added.
    '''
    def start(self):
        with self._lock:
            assert not self._started
            self._started = True
            for _ in range(min(self._num_threads, len(self._calls))):
                self._workers.append(WorkerThread(self,
                                                  self._callback_start,
                                                  self._callback_done))
            for thread in self._workers:
                thread.start()

    '''
    Returns a 3-tuple specifying the number of (pending, active, done) threads.
    '''
    def stats(self):
        with self._lock:
            pending, active, done = len(self._calls), 0, 0
            for worker in self._workers:
                if worker.isAlive(): active += 1
                else: done += 1
        return pending, active, done

    ''' Returns the names of the currently active calls. '''
    def get_active_calls(self):
        active_calls = ''
        with self._lock:
            for worker in self._workers:
                name = worker.get_active_call()
                if name:
                    if len(active_calls) > 0:
                        active_calls += ', '
                    active_calls += name
        return active_calls

    ''' Joins all worker threads. '''
    def join(self):
        with self._lock:
            assert self._started
        for worker in self._workers:
            worker.join()

    '''
    Returns the next call, or None if there are no more calls. Called only by
    the worker threads.
    '''
    def _next_call(self):
        with self._lock:
            assert self._started
            if self._calls:
                return self._calls.pop(0)
            return None
